package com.example.demo.redis;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;

/**
 * redis分布式锁Service
 * 1、使用lua脚本实现加锁和解锁操作，保证原子性
 * 2、支持锁自动续期
 */
@Service
@Slf4j
public class RedisLockService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 加锁的lua脚本
     */
    private static final RedisScript<Long> LOCK_LUA_SCRIPT = new DefaultRedisScript<>(
            "if redis.call(\"setnx\", KEYS[1], KEYS[2]) == 1 then return redis.call(\"expire\", KEYS[1], KEYS[3]) else return -1 end"
            , Long.class
    );

    /**
     * 解锁的lua脚本
     */
    private static final RedisScript<Long> UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>(
            "if redis.call(\"get\",KEYS[1]) == KEYS[2] then return redis.call(\"del\",KEYS[1]) else return -1 end"
            , Long.class
    );

    /**
     * 锁续期的lua脚本
     */
    private static final RedisScript<Long> RENEW_LUA_SCRIPT = new DefaultRedisScript<>(
            "if redis.call(\"get\",KEYS[1]) == KEYS[2] then return redis.call(\"expire\", KEYS[1], KEYS[3]) else return -1 end"
            , Long.class
    );

    /**
     * 锁过期时间(秒）
     */
    private static final Long LOCK_EXPIRE_TIME = 30L;

    /**
     * 锁自动续期线程池
     */
    private static final ScheduledExecutorService LOCK_RENEW_EXECUTOR = new ScheduledThreadPoolExecutor(2);

    /**
     * 存储锁自动续期线程池的future对象
     */
    private static final Map<String, ScheduledFuture<?>> FUTURE_MAP = new ConcurrentHashMap<>();

    /**
     * 带锁执行逻辑，加锁成功执行，失败不执行
     *
     * @param command
     * @param key
     * @param value
     * @return
     */
    public boolean runWithLock(Runnable command, String key, String value) {
        boolean lock;
        try {
            lock = this.lock(key, value);
            if (lock) {
                command.run();
            }
        } finally {
            this.unlock(key, value);
        }
        return lock;
    }

    /**
     * 加锁方法
     * 对key加锁，value为key对应的值，支持锁自动续期
     *
     * @param key   key
     * @param value value
     * @return
     */
    public boolean lock(String key, String value) {
        if (key == null || value == null) {
            return false;
        }
        List<String> keys = Arrays.asList(key, value, LOCK_EXPIRE_TIME.toString());
        Long execute = redisTemplate.execute(LOCK_LUA_SCRIPT, keys);
        boolean success = execute != null && execute > 0;
        log.info("lock key={} value={} {}", key, value, success ? "success" : "fail");
        if (success) {
            //加锁成功设置自动续期
            ScheduledFuture<?> scheduledFuture = this.renew(key, value, LOCK_EXPIRE_TIME / 3);
            FUTURE_MAP.put(key, scheduledFuture);
        }
        return success;
    }

    /**
     * 解锁方法
     * 对key解锁，只有value值等于redis中key对应的值才能解锁，避免误解锁
     *
     * @param key
     * @param value
     * @return
     */
    public boolean unlock(String key, String value) {
        if (key == null || value == null) {
            return false;
        }
        List<String> keys = Arrays.asList(key, value);
        Long execute = redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys);
        //返回值0为锁已经不存在，也认为解锁成功
        boolean success = execute != null && execute >= 0;
        log.info("unlock key={} value={} {}", key, value, success ? "success" : "fail");
        //终止续期任务
        ScheduledFuture<?> scheduledFuture = FUTURE_MAP.remove(key);
        if (scheduledFuture != null) {
            scheduledFuture.cancel(false);
        }
        return success;
    }

    /**
     * 锁续期方法
     * 对key续期，只有value值等于redis中key对应的值才进行续期
     *
     * @param key
     * @param value
     * @param delay 续期间隔（秒）
     * @return
     */
    private ScheduledFuture<?> renew(String key, String value, long delay) {
        return LOCK_RENEW_EXECUTOR.scheduleWithFixedDelay(() -> {
            if (key == null || value == null) {
                return;
            }
            List<String> keys = Arrays.asList(key, value, LOCK_EXPIRE_TIME.toString());
            Long execute = redisTemplate.execute(RENEW_LUA_SCRIPT, keys);
            boolean success = execute != null && execute > 0;
            log.info("renew key={} value={} {}", key, value, success ? "success" : "fail");
            if (!success) {
                //续期失败，终止任务
                ScheduledFuture<?> scheduledFuture = FUTURE_MAP.remove(key);
                if (scheduledFuture != null) {
                    scheduledFuture.cancel(false);
                }
            }
        }, delay, delay, TimeUnit.SECONDS);
    }

}
